package net.ib.mtalk.network;

import java.io.File;

import net.ib.mntalk.protocol.Common;
import net.ib.mntalk.protocol.Common.AccessToken;
import net.ib.mntalk.protocol.Session.AcquireAccessTokenRes;
import net.ib.mntalk.protocol.Session.AcquireAccountTokenRes;
import net.ib.mntalk.protocol.Session.AuthenticateReq;
import net.ib.mntalk.protocol.Session.AuthenticateRes;
import net.ib.mtalk.constant.MTalkConstant;
import net.ib.mtalk.constant.NetworkConstant;
import net.ib.mtalk.constant.ServiceConstant;
import net.ib.mtalk.database.DatabaseHelper;
import net.ib.mtalk.global.GlobalApplication;
import net.ib.mtalk.global.GlobalPreferences;
import net.ib.mtalk.network.MethodId.Session;
import net.ib.mtalk.network.clientapi.DeliverMessages;
import net.ib.mtalk.network.clientapi.DeliverReadMarks;
import net.ib.mtalk.network.clientapi.Ping;
import net.ib.mtalk.network.task.AuthenticateTask;
import net.ib.mtalk.network.task.GetDomainNameTask;
import net.ib.mtalk.network.task.SetUserDeviceInfoTask;
import net.ib.mtalk.service.SelecterService;
import net.ib.mtalk.util.MTalkLogWriter;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.NotificationManager;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;

/**
 * 프로토콜 특정 상태 관리<br>
 * <br>
 * 프로토콜 내용상 반드시 있어야 하는 작업들을 해준다.
 * @author Arngard
 * */
public class NetFlowControl {
	
	/**
	 * 네트워크 요청이 시작되기 전에, UI 스레드에서 호출되어야 한다. application 객체 생성시 불러주는 것이 좋다.
	 */
	public static void readyClientApi() {
		MTalkLogWriter.v("NetFlowControl.readyClientApi() - STT");
		DeliverMessages.setDefault();
		DeliverReadMarks.setDefault();
		Ping.setDefault();
		MTalkLogWriter.v("NetFlowControl.readyClientApi() - END");
	}

	/**
	 * 네트워크(세션서버)로부터 들어오는 모든 메시지를 감시하고 관련 작업을 해 준다.
	 * @param tcpEnvelope
	 */
	public static void handleResponse(TcpEnvelope tcpEnvelope) {
		if (tcpEnvelope == null) {
			return;
		}
		if (tcpEnvelope.getType() != TcpEnvelope.RESPONSE) {
			return;
		}

		int methodId = tcpEnvelope.getMethodId();

		try {
			switch (methodId) {
			case Session.Registration.AcquireAccountToken:
				setAppId((AcquireAccountTokenRes) tcpEnvelope.getMessage());
				break;
			case Session.Authentication.AcquireAccessToken:
				// 새로운 AccessToken 얻었을 때 저장해주기
				setAccessToken((AcquireAccessTokenRes) tcpEnvelope.getMessage());
				break;
			case Session.Authentication.Authenticate:
				// 로그인시 로그인 상태 세팅하고 관련 작업 해주기
				setAuth((AuthenticateRes) tcpEnvelope.getMessage());
				break;
			default:
				// do nothing
				break;
			}
		} catch(Exception e) {
			MTalkLogWriter.e(e);
		}
	}
	
	/**
	 * 세션 서버 연결에 실패하였다면, 세션 접속 주소를 초기화한다.
	 * */
	public static void resetSessionAddress() {
		/* 망이 광범위하게 다운된 경우 모든 클라이언트는 자기 주소를 초기화하고 다음번엔 스위치에 접속할 것이다.
		 * 그래도 되는가? 이 주소를 가능한한 재활용해야 하는 것 아닐까?
		 * */
		TcpClient.unsetDomain();
		TcpClient.unsetPort();
	}
	
	/**
	 * 세션 서버 연결이 기본 주소에 수립된 경우, 새로운 주소를 할당받아서 저장해둬야 한다.
	 * 다음에 접속할 때는 새로 받은 주소로 접속할 것이다.
	 * 이 함수는 접속 성공시 불려야 한다.
	 * */
	public static void checkSessionAddress() {
		if (NetworkConstant.Session.BASE_DOMAIN.equals(TcpClient.getDomain())) {	// 지금 접속에 쓴 주소가 기본 주소로 세팅되어 있다면
			new GetDomainNameTask(GlobalApplication.getContext()).execute();	// 새 주소를 요청하는 작업을 한다.
			// 쿼리 결과는 task 내부에서 알아서 했을 것이다.
		}
	}

	/**
	 * 매 연결 수립시 반드시 로그인을 해야 한다.
	 * 이 로그인은 연결에 귀속된 인증이다. 연결이 끊기면 무효화된다. 1개 쿼리로 처리된다.<br>
	 * <br>
	 * 로그인이 되지 않은 상태라고 해서 다른 쿼리를 막으면 안 된다.
	 * 로그인 실패는 UI 말단에 보고되어 사용자의 행동을 요구하는 작업이어야 한다.<br>
	 * <br>
	 * 로그인은 한 연결에 대해서 중복으로 이루어질 수 없다. 재로그인을 시도하면 세션이 새로 수립될 것이기 때문이다.
	 * 따라서 클라이언트는 현재 로그인된 상태인지 아닌지를 말단에서 확인할 방법이 필요하다.<br>
	 */
	private static boolean authValid = false;

	/** 인증이 진행중인지 확인하기 위한 플래그 */
	private static long authOnTry = Long.MIN_VALUE;

	/**
	 * @return 로그인이 되어 있는 상태이면 true
	 */
	public static boolean hasAuth() {
		return authValid;
	}

	/**
	 * 주어진 AuthenticateRes로 인증 상태를 갱신하고 필요한 작업을 한다.
	 * @param authRes
	 */
	public static void setAuth(AuthenticateRes authRes) {
		authOnTry = Long.MIN_VALUE;
		try {
			int responseCode = authRes.getResponseCode();
			switch (responseCode) {
			case Common.SUCCESS:
				authValid = true;
				MTalkLogWriter.i("NetFlowControl.setAuth(): Auth ON");

				if (! GlobalPreferences.isRegistrationIdLocated()) {	// C2DM 이 우리 서버에 등록되지 않았다.
					/* 다음은 재현 가능한 문제이며, 이 블록은 이에 대한 대응을 위해 존재한다:
					 * 
					 * 이 경우 서비스는 셀렉트를 위한 루틴을 시작한다.
					 * 우선 핑을 날려 로그인 트리거를 한다.
					 * 그 직후 핑은 성공하고, 동시에 로그인도 시도된다.
					 * 그리고 이쯤에 서비스는 셀렉트를 시도하는데, 타이밍 문제가 발생한다.
					 * 로그인이 되었다면 상관 없지만, 로그인이 되지 않았다면 실패하고 침묵할 것이다.
					 * 이 경우를 대비하여 이곳에서 서비스를 한 번 호출해주어야 할 것이다.
					 * */
					if (GlobalPreferences.getRegistrationId() != null) {	// C2DM Registration ID를 가지고 있다.
						// 이 때는 서비스의 시작을 보장할 게 아니라 우리 서버에게 C2DM 통보를 해 줘야 한다.
						new SetUserDeviceInfoTask(GlobalApplication.getContext()).execute();
					} else {	// 아예 구글측에서 C2DM Registration ID가 발급되지 않았다: 아마도 C2DM사용 불가능한 단말 같다.
						// 이 때는 서비스의 시작을 보장해줘야 한다.
						MTalkLogWriter.d("NetFlowControl.removeAuth(): Has no C2DM Registration. Service start based on Auth");
						SelecterService.actionStart(GlobalApplication.getContext());//LocalService.actionStart(GlobalApplication.getContext());
					}
				} else {
					/* C2DM이 우리 서버에까지 등록되어 있다면 서비스가 1-셀렉트 행동을 하지 않을 것이다.
					 * 그렇다면 이 로그인은 4-셀렉트 또는 서비스 외 모듈에 의해 유발된 것이다.
					 * 4-셀렉트의 경우를 위하여 한 번 호출해주자.
					 * 어차피 4셀렉트 모드가 아니면 실제 작업은 이루어지지 않을 것이다.
					 * */
					MTalkLogWriter.d("NetFlowControl.removeAuth(): Retry to start service based on Auth");
					SelecterService.actionStart(GlobalApplication.getContext());//LocalService.actionStart(GlobalApplication.getContext());
				}
				break;
			default:
				authValid = false;
				MTalkLogWriter.i("NetFlowControl.setAuth(): Auth Token invalid. responseCode="+responseCode);
				// 인증 해지 루틴
				removeAuthInfo(GlobalApplication.getContext());
				break;
			}
		} catch(Exception e) {
			MTalkLogWriter.e(e);
			authValid = false;
		}
	}

	/**
	 * 클라이언트의 인증 상태를 해제한다. 이 호출은 클라이언트의 연결 관리 대응에 영향을 끼친다. 서버측의 인식과는 무관하다.
	 */
	public static void removeAuth() {
		MTalkLogWriter.i("NetFlowControl.removeAuth(): Auth OFF");
		authValid = false;
		authOnTry = Long.MIN_VALUE;
		SelecterService.actionStop(GlobalApplication.getContext());	// 로그인이 풀렸다면 어차피 셀렉트는 걸지 못한다.
	}

	/**
	 * @return {@link #tryGetAuth(NetResultReceiver) tryGetAuth(null)}와 같다.
	 */
	synchronized public static boolean tryGetAuth() {
		return tryGetAuth(null);
	}

	/**
	 * AuthenticateTask를 이용하여 로그인 요청을 한다.
	 * 기존 로그인 체크나 리턴 방식 등은, 이 클래스 내의 이름이 같은 다른 메소드들과 같다.
	 * @param context AuthenticateTask의 생성에 쓰일 인자
	 * @param handler AuthenticateTask의 생성에 쓰일 인자
	 * @return 이번의 호출에 의해 로그인 시도를 새로 했으면 true.<br>
	 * <font color=#ff0000>Caution!</font> false가 리턴된 경우, 네트워크 요청 자체가 이루어지지 않은 것이므로 콜백을 기다리면 안 된다.
	 */
	synchronized public static boolean tryGetAuth(Context context, Handler handler) {
		MTalkLogWriter.v("NetFlowControl.tryGetAuth() - STT");
		
		if (hasAuth()) {	// 로그인되어 있다면
			MTalkLogWriter.v("NetFlowControl.tryGetAuth() - END with no work. Already has Auth.");
			return false;
		}
		
		AccessToken token = GlobalPreferences.getMessageFromPreferences(AccessToken.class);
		if (token == null) {	// 토큰이 없다면
			MTalkLogWriter.v("NetFlowControl.tryGetAuth() - END with no work. Has no AccessToken.");
			return false;
		}
		
		long currentTime = android.os.SystemClock.elapsedRealtime();
		if (currentTime < authOnTry + NetworkConstant.Session.TIMEOUT_MILLIS) {	// 이미 요청이 진행중인 경우
			MTalkLogWriter.v("NetFlowControl.tryGetAuth() - END with no work. Already on try.");
			return false;
		}
		authOnTry = currentTime;
		
		// 토큰 있는데 로그인 안 되어 있으면
		AuthenticateReq authenticateReq = new AuthenticateReq();
		authenticateReq.setAccessToken(token);
		authenticateReq.setApiVersion(MTalkConstant.API_VERSION);
		authenticateReq.setDeviceType(Common.ANDROID);

		AuthenticateTask task = new AuthenticateTask(context, handler, authenticateReq);
		MTalkLogWriter.d("NetFlowControl.tryGetAuth(): try to get Auth");
		task.execute();

		MTalkLogWriter.v("NetFlowControl.tryGetAuth() - END");
		return true;
	}

	/**
	 * 로그인 요청을 한다.
	 * 만일 이미 이 메소드에 의해 로그인이 진행중이거나, 로그인되어 있는 경우, 아무 일도 하지 않는다.<br>
	 * @param callback 로그인 요청의 결과가 전달될 콜백
	 * @return 이번의 호출에 의해 로그인 시도를 새로 했으면 true.<br>
	 * <font color=#ff0000>Caution!</font> false가 리턴된 경우, 네트워크 요청 자체가 이루어지지 않은 것이므로 콜백을 기다리면 안 된다.
	 */
	synchronized public static boolean tryGetAuth(NetResultReceiver callback) {
		MTalkLogWriter.v("NetFlowControl.tryGetAuth() - STT");
		
		if (hasAuth()) {	// 로그인되어 있다면
			MTalkLogWriter.v("NetFlowControl.tryGetAuth() - END with no work. Already has Auth.");
			return false;
		}
		
		AccessToken token = GlobalPreferences.getMessageFromPreferences(AccessToken.class);
		if (token == null) {	// 토큰이 없다면
			MTalkLogWriter.v("NetFlowControl.tryGetAuth() - END with no work. Has no AccessToken.");
			return false;
		}
		
		long currentTime = android.os.SystemClock.elapsedRealtime();
		if (currentTime < authOnTry + NetworkConstant.Session.TIMEOUT_MILLIS) {	// 이미 요청이 진행중인 경우
			MTalkLogWriter.v("NetFlowControl.tryGetAuth() - END with no work. Already on try.");
			return false;
		}
		authOnTry = currentTime;
		
		// 토큰 있는데 로그인 안 되어 있으면
		AuthenticateReq authenticateReq = new AuthenticateReq();
		authenticateReq.setAccessToken(token);
		authenticateReq.setApiVersion(MTalkConstant.API_VERSION);
		authenticateReq.setDeviceType(Common.ANDROID);

		TcpEnvelope revelop = new TcpEnvelope.Builder()
		.setRequestId(TcpEnvelope.getNewRequestId())
		.setMethodId(Session.Authentication.Authenticate)
		.setType(TcpEnvelope.REQUEST)
		.setMessage(authenticateReq)
		.build();

		if (callback != null) {
			revelop.setCallback(callback);
		}

		MTalkLogWriter.d("NetFlowControl.tryGetAuth(): try to get Auth");
		TcpClient.send(revelop, true);

		MTalkLogWriter.v("NetFlowControl.tryGetAuth() - END");
		
		return true;
	}
	
	/**
	 * 주어진 AcquireAccountTokenRes로부터 APP ID를 추출하여 저장한다.
	 * @param res
	 * @return
	 */
	public static boolean setAppId(AcquireAccountTokenRes res) {
		try {
			if (res.getResponseCode() == Common.SUCCESS) {
				// 지연된 C2DM 등록에 사용하기 위해 앱 ID를 저장해둔다.
				MTalkLogWriter.d("NetFlowControl.setAppId(): I've got an App ID.");
				GlobalPreferences.setAppUUID(res.getAccountToken().getAppId());
				return true;
			}
		} catch(Exception e) {
			MTalkLogWriter.e(e);
		}
		return false;
	}

	/**
	 * 주어진 AcquireAccessTokenRes으로부터 AccessToken을 추출하여 저장한다.
	 * @param token
	 * @return
	 */
	public static boolean setAccessToken(AcquireAccessTokenRes token) {
		try {
			if (token.getResponseCode() == Common.SUCCESS) {
				MTalkLogWriter.d("NetFlowControl.setAccessToken(): I've got a token.");
				GlobalPreferences.putMessageToPreferences(token.getAccessToken());
				return true;
			}
		} catch(Exception e) {
			MTalkLogWriter.e(e);
		}
		return false;
	}
	
	/**
	 * 네트워크 접속 등의 네트워크 동작이나 트래픽이 발생했음을 서비스에 알린다.
	 * 서비스는 이 호출을 네트워크가 유휴 상태인지를 판단하기 위한 정보로 사용한다.
	 * 이 메소드의 호출은 유통기한이 지난 콜백을 회수하는 역할을 병행한다.
	 * @param context
	 * @throws SecurityException
	 */
	public static void trafficControl(Context context) throws SecurityException {
		TcpClient.expiredCollector();
		SelecterService.actionTraffic(context);
	}
	
	public static void removeAuthInfo(Context context)
	{
		NotificationManager notiManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
		notiManager.cancel(ServiceConstant.NOTI_ID_MSG_RECV);
		
		NetFlowControl.removeAuth();
		DatabaseHelper.getInstance(context).clearTable(context);
		
		new File("/data/data/"+context.getPackageName()+"/shared_prefs/"+context.getPackageName()+"_preferences.xml").delete();
		new File("/data/data/"+context.getPackageName()+"/databases/mtalk.db").delete();
		if(context instanceof Activity)
			((Activity) context).moveTaskToBack(true);
		android.os.Process.killProcess(android.os.Process.myPid()); 

		ActivityManager am = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE); 
		am.restartPackage(context.getPackageName()); 
	}
	
	/**
	 * 
	 * @return 네트워크 연결이 되어 있으면 true, 그렇지 않으면 false
	 */
	public static boolean checkNetWorkStatus(Context context)
	{
		boolean isWifiConn = false, isMobileConn = false;
		ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
		NetworkInfo ni = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
		if(ni != null)
			isWifiConn = ni.isConnected();
		
		ni = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
		if(ni != null)
			isMobileConn = ni.isConnected();
			           		  
		if(isWifiConn==false && isMobileConn==false)
			   return false;
		
		return true;
	}

}
